// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Linq;
using JetBrains.Annotations;
using Oracle.ManagedDataAccess.Client;
using System.Threading;
using System.Threading.Tasks;
using System.Linq.Expressions;

namespace SnapObjects.Data.Oracle
{
    internal class OracleAdoDbCommand : AdoDbCommand
    {
        public OracleAdoDbCommand(
            [NotNull] string sqlText,
            IEnumerable<IAdoDbParameter> parameters,
            bool addExceptionToTransaction,
            CommandType commandType = CommandType.Text)
            : base(sqlText, parameters, addExceptionToTransaction, commandType)
        {
        }

        protected override string BuildNoExecCommandText(DataContext context, string commandText)
        {
            return $"explain plan for {commandText}";
        }

        protected override void PreCreateCommand(DataContext dataContext)
        {
            if (this.Parameters.Count() > 1)
            {
                var tokenizer = new SqlSyntaxTokenizer(this.SqlText);

                var parameterPlaceholder = dataContext.AdoDbSqlProvider.SqlGenerationHelper.ParameterIdentifier.ToString();

                var adoDbParameters = new List<IAdoDbParameter>();

                for (var i = 0; i < tokenizer.Tokens.Count; i++)
                {
                    var adoDbParameter = this.Parameters.FirstOrDefault(
                        p => p.ParameterName == tokenizer.Tokens[i]);

                    if (adoDbParameter != null)
                    {
                        adoDbParameters.Add(adoDbParameter);
                    }
                }

                if (adoDbParameters.Count > 0 && adoDbParameters.Count > this.Parameters.Count())
                {
                    this.Parameters = adoDbParameters;
                }
            }
        }

        public override int ExecuteNonQuery(DataContext dataContext)
        {
            var ret = base.ExecuteNonQuery(dataContext);

            return ret == -1 ? 0 : ret;
        }

        public async override Task<int> ExecuteNonQueryAsync(
            DataContext dataContext, CancellationToken cancellationToken = default)
        {
            var ret = await base.ExecuteNonQueryAsync(dataContext, cancellationToken);

            return ret == -1 ? 0 : ret;
        }

        protected override DbCommand CreateCommand(DataContext dataContext)
        {
            var command = base.CreateCommand(dataContext);

            var options = (OracleDataContextOptions)dataContext.ContextOptions;

            ((OracleCommand)command).InitialLONGFetchSize = options.InitialLongFetchSize;
            ((OracleCommand)command).InitialLOBFetchSize = options.InitialLOBFetchSize;

            if(this.CommandType == CommandType.StoredProcedure && options.SPBindingByName)
            {
                ((OracleCommand)command).BindByName = true;
            }

            return command;
        }

        public override object ExecuteScalar(DataContext dataContext)
        {
            object ret = null;

            OracleAdoDbDataVisitor dataVisitor = null;

            dataContext.CurrentConnection.Open();

            try
            {
                var tuple = (Tuple<DbCommand, DbDataReader>)this.Execute(
                    dataContext, AdoDbCommandMethod.ExecuteReader, CommandBehavior.SingleRow);

                dataVisitor = new OracleAdoDbDataVisitor(
                    (OracleCommand)tuple.Item1,
                    (OracleDataReader)tuple.Item2,
                    this.Parameters,
                    dataContext.ContextOptions);

                if (dataVisitor.MoveNext())
                {
                    var reader = dataVisitor.ReadData();

                    ret = this.GetScalarValue(reader, dataContext);
                }
            }
            finally
            {
                if (dataVisitor != null)
                {
                    dataVisitor.Dispose();
                }

                dataContext.CurrentConnection.Close();
            }

            return ret;
        }

        public async override Task<object> ExecuteScalarAsync(DataContext dataContext, CancellationToken cancellationToken = default)
        {
            object ret = null;

            OracleAdoDbDataVisitor dataVisitor = null;

            await dataContext.CurrentConnection.OpenAsync(cancellationToken);

            try
            {
                var tuple = await this.ExecuteQueryAsync(dataContext, CommandBehavior.SingleRow, cancellationToken);

                dataVisitor = new OracleAdoDbDataVisitor(
                    (OracleCommand)tuple.Item1, 
                    (OracleDataReader)tuple.Item2, 
                    this.Parameters, 
                    dataContext.ContextOptions);

                if (await dataVisitor.MoveNextAsync(cancellationToken))
                {
                    var reader = dataVisitor.ReadData();

                    ret = this.GetScalarValue(reader, dataContext);
                }
            }
            finally
            {
                if (dataVisitor != null)
                {
                    await dataVisitor.DisposeAsync();
                }

                await dataContext.CurrentConnection.CloseAsync();
            }

            return ret;
        }

        protected override void ConfigDbDataReader(DbDataReader reader, IDataContextOptions options)
        {
            base.ConfigDbDataReader(reader, options);

            var type = reader.GetType();

            var property = type.GetProperty("SuppressGetDecimalInvalidCastException");

            if (property != null && reader is OracleDataReader orclReader)
            {
                var readerParameter = Expression.Parameter(typeof(OracleDataReader));
                var valueParameter = Expression.Parameter(typeof(bool));

                var setter = Expression.Lambda<Action<OracleDataReader, bool>>(
                    Expression.Assign(Expression.MakeMemberAccess(readerParameter, property), valueParameter),
                    readerParameter,
                    valueParameter)
                    .Compile();

                setter(orclReader, ((OracleDataContextOptions)options).SuppressGetDecimalInvalidCastException);
            }
        }

        protected override int GetSqlDbCode(Exception innerException)
        {
            if (innerException is OracleException ex)
            {
                return ex.Number;
            }

            return -1;
        }

        private object GetScalarValue(IDataReader reader, DataContext dataContext)
        {
            var value = reader.GetValue(0);

            if (value?.GetType() == typeof(DBNull))
            {
                return null;
            }

            return value;
        }
    }
}
